Este proyecto tiene como objetivo procesar y transformar el conjunto de datos geográficos del Mapa Escolar, con el fin de integrarlos en el ecosistema de OpenStreetMap (OSM) mediante la plataforma MapRoulette.
El flujo de trabajo incluye la limpieza y estandarización del dataset original, el enriquecimiento de los registros con etiquetas (tags) compatibles con el esquema de OSM (como amenity=school, isced:level, operator:type, entre otros), y la generación de una capa geoespacial apta para su análisis y edición colaborativa.
Posteriormente, los datos son convertidos a un formato GeoJSON que puede ser cargado en MapRoulette, permitiendo a la comunidad validar, corregir y completar los datos directamente sobre el mapa, respetando las convenciones del modelado en OSM.
Proyecto en GitHub
De esta manera, se busca mejorar la cobertura, precisión y semántica de la información educativa en OpenStreetMap, especialmente en zonas donde los datos están incompletos o desactualizados.
library('sf')
## Linking to GEOS 3.12.1, GDAL 3.8.4, PROJ 9.4.0; sf_use_s2() is TRUE
#sudo apt update
#sudo apt install build-essential libgdal-dev libgeos-dev libproj-dev libudunits2-dev libglpk-dev
#install.packages("Rcpp")
#install.packages("terra")
#install.packages("mapview")
#install.packages("geosphere")
#install.packages("shadowtext")
#install.packages("osmdata")
library(dplyr)
##
## Adjuntando el paquete: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library('mapview')
library(geosphere)
library(stringr)
library(grid)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.0 ✔ readr 2.1.5
## ✔ ggplot2 3.4.4 ✔ tibble 3.2.1
## ✔ lubridate 1.9.3 ✔ tidyr 1.3.1
## ✔ purrr 1.0.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ purrr::%||%() masks base::%||%()
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(shadowtext)
library(osmdata)
## Data (c) OpenStreetMap contributors, ODbL 1.0. https://www.openstreetmap.org/copyright
BLUE <- "#076fa2"
RED <- "#E3120B"
BLACK <- "#202020"
GREY <- "grey50"
gdf <- st_read("Unidades Educativas Locales 30-06-2025.geojson")
## Reading layer `Unidades Educativas Locales 30-06-2025' from data source
## `/home/juan/PLAT/PIRS/Escuelas/Escuelas/Unidades Educativas Locales 30-06-2025.geojson'
## using driver `GeoJSON'
## Simple feature collection with 21568 features and 55 fields
## Geometry type: POINT
## Dimension: XY
## Bounding box: xmin: -63.38269 ymin: -40.8115 xmax: -56.67471 ymax: -33.29664
## Geodetic CRS: WGS 84
#Dataset reducido
#gdf <- gdf[4000:4300,]
Solamente me quedo con las columnas que pueden ser importadas a OpenStreetMap
columnas_clave <- c("cueanexo",
"nombre","calle","nro","calle_lateral_derecha",
"calle_lateral_izquierda","localidad","codigo_postal",
"caracteristica_telefonica","nro_telefono",
"email","sector","cui",
"latitud","longitud","nivel","turnos","id_nivel","geometry")
dataset_reducido <- gdf[, c(columnas_clave)]
#mapview(dataset_filtrado)
Para Advertir al mapeador de que posiblemente haya multiples instituciones en esa ubicacion geografica:
Esta toma 4 minutos procesarlo
#filtro para tener solo jardines, primarias, secundarias y superiores,
df <- dataset_reducido[dataset_reducido$id_nivel < 6, ]
#saco los lvl 4
#df <- df %>%
# filter(id_nivel != 4)
# Crear la columna vacía
df$cercania <- NA
df$cercanos <- NA
# Calcular para cada fila
for (i in 1:nrow(df)) {
# Coordenadas del registro i
coord_i <- c(df$longitud[i], df$latitud[i])
# Calcula distancias a todos los registros
distancias <- distHaversine(coord_i, cbind(df$longitud, df$latitud))
# Elimina su propia distancia
distancias[i] <- Inf
# Cuenta cuántos están a menos de 100 m
cantidad <- sum(distancias < 100)
# Asigna a la columna "cercanos"
df$cercanos[i] <- cantidad
# Categoriza en "cercania"
if (cantidad > 0) {
if(cantidad == 1){
df$cercania[i] <- paste( "Hay un registro cercano en un radio de 100m")
}else{
df$cercania[i] <- paste( "Hay", cantidad, "registros cercanos en un radio de 100m")
}
} else {
df$cercania[i] <- "No hay registros cercanos en un radio de 100m"
}
}
#table(df$cercania)
barplot(table(df$cercanos),
main = "Cercanias",
xlab = "Cantidad",
ylab = "Frecuencia Absoluta",
col = "lightblue",
border = "black")
df$cercanos <- as.factor(df$cercanos) #tiene que estar como factor para que lo tome
#mapview(df)
#mapview(df, zcol = "cercanos")
#agarro una columna para meter despues otras
dosm <- df[, "email"]
dosm$latitud <- df$latitud
dosm$longitud <- df$longitud
df <- df %>%
mutate(`isced:level` = as.integer(case_when(
id_nivel == 1 ~ 0,
id_nivel == 2 ~ 1,
id_nivel == 3 ~ 2,
id_nivel == 4 ~ 3,
id_nivel == 5 ~ 2
)))
dosm$`isced:level` <- df$`isced:level`
dosm <- dosm %>%
mutate(`amenity` = case_when(
`isced:level` == 0~ "kindergarten",
`isced:level` == 1 ~ "school",
`isced:level` == 2 ~ "school",
`isced:level` == 3 ~ "college",
))
#Dedicada para instruccion
dosm <- dosm %>%
mutate(`tipo` = case_when(
`isced:level` == 0~ "el jardín",
`isced:level` == 1 ~ "la escuela",
`isced:level` == 2 ~ "la escuela",
`isced:level` == 3 ~ "el colegio",
))
df <- df %>%
mutate(`operator:type` = case_when(
sector == 'Estatal' ~ "public",
sector == 'Privado' ~ 'private'
))
dosm$`operator:type` <- df$`operator:type`
df <- df %>%
mutate(`addr:housenumber` = case_when(
nro == 'S/N' ~ "No hay altura registrada",
nro != 'S/N' ~ nro
))
dosm$`addr:housenumber` <- df$`addr:housenumber`
dosm$`ref:cue` <- df$cueanexo
dosm$`ref:cui` <- df$cui
dosm$`ref:cue` <- str_replace_all(dosm$`ref:cue`, ",", ";")
dosm$`ref:cui` <- str_replace_all(dosm$`ref:cui`, ",", ";")
dosm$`addr:postcode` <- df$codigo_postal
#Deteccion de escuelas especiales
df <- df %>%
mutate(es_especial = ifelse(
str_detect(nombre, regex("ESPECIAL", ignore_case = TRUE)),
" * education_for:disabled=yes",
""))
dosm$`education_for:disabled` <- df$es_especial
df <- df %>%
mutate(`opening_hours` = case_when(
turnos == '"DOBLE ESCOLARIDAD",INTERMEDIO' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD"' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD",INTERMEDIO,MAÑANA' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD",INTERMEDIO,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == '"DOBLE ESCOLARIDAD",INTERMEDIO,MAÑANA,TARDE' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD",MAÑANA' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD",MAÑANA,NOCHE' ~ "Mo-Fr 08:00-22:30",
turnos == '"DOBLE ESCOLARIDAD",MAÑANA,NOCHE,TARDE' ~ "Mo-Fr 08:00-22:30",
turnos == '"DOBLE ESCOLARIDAD",MAÑANA,TARDE' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD",MAÑANA,TARDE,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == '"DOBLE ESCOLARIDAD",MAÑANA,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == '"DOBLE ESCOLARIDAD",TARDE' ~ "Mo-Fr 08:00-16:00",
turnos == '"DOBLE ESCOLARIDAD",TARDE,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == '"DOBLE ESCOLARIDAD",VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == 'ALTERNADO,"DOBLE ESCOLARIDAD"' ~ "Mo-Fr 08:00-16:00",
turnos == 'ALTERNADO' ~ "Mo-Fr 08:00-16:00",
turnos == 'ALTERNADO,"DOBLE ESCOLARIDAD",MAÑANA' ~ "Mo-Fr 08:00-16:00",
turnos == 'ALTERNADO,"DOBLE ESCOLARIDAD",MAÑANA,TARDE' ~ "Mo-Fr 08:00-16:00",
turnos == 'ALTERNADO,INTERMEDIO,TARDE' ~ "Mo-Fr 12:00-16:00",
turnos == 'ALTERNADO,MAÑANA' ~ "Mo-Fr 08:00-12:00",
turnos == 'ALTERNADO,MAÑANA,NOCHE' ~ "Mo-Fr 08:00-12:00;Mo-Fr 18:30-22:30",
turnos == 'ALTERNADO,MAÑANA,TARDE,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == 'ALTERNADO,MAÑANA,TARDE' ~ "Mo-Fr 08:00-16:00",
turnos == 'ALTERNADO,NOCHE,VESPERTINO' ~ "Mo-Fr 16:00-22:30",
turnos == 'ALTERNADO,TARDE' ~ "Mo-Fr 12:00-16:00",
turnos == 'ALTERNADO,TARDE,VESPERTINO' ~ "Mo-Fr 16:00-21:30",
turnos == 'ALTERNADO,VESPERTINO' ~ "Mo-Fr 17:30-21:30",
turnos == 'INTERMEDIO' ~ "Mo-Fr 12:00-17:00",
turnos == 'INTERMEDIO,MAÑANA' ~ "Mo-Fr 08:00-17:00",
turnos == 'INTERMEDIO,MAÑANA,TARDE' ~ "Mo-Fr 08:00-17:00",
turnos == 'INTERMEDIO,MAÑANA,TARDE,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == 'INTERMEDIO,MAÑANA,VESPERTINO' ~ "Mo-Fr 08:00-12:00;Mo-Fr 17:30-21:30",
turnos == 'INTERMEDIO,TARDE' ~ "Mo-Fr 12:00-17:00",
turnos == 'INTERMEDIO,VESPERTINO' ~ "Mo-Fr 12:00-21:30",
turnos == 'MAÑANA' ~ "Mo-Fr 08:00-12:00",
turnos == 'MAÑANA,NOCHE,TARDE' ~ "Mo-Fr 08:00-22:30",
turnos == 'MAÑANA,NOCHE' ~ "Mo-Fr 08:00-12:00; Mo-Fr 18:30-22:30",
turnos == 'MAÑANA,NOCHE,TARDE,VESPERTINO' ~ "Mo-Fr 08:00-22:30",
turnos == 'MAÑANA,TARDE' ~ "Mo-Fr 08:00-17:00",
turnos == 'MAÑANA,TARDE,VESPERTINO' ~ "Mo-Fr 08:00-21:30",
turnos == 'MAÑANA,VESPERTINO' ~ "Mo-Fr 08:00-12:00; Mo-Fr 17:30-21:30",
turnos == 'NOCHE' ~ "Mo-Fr 18:30-22:30",
turnos == 'NOCHE,TARDE' ~ "Mo-Fr 13:00-22:30",
turnos == 'NOCHE,TARDE,VESPERTINO' ~ "Mo-Fr 13:00-22:30",
turnos == 'NOCHE,VESPERTINO' ~ "Mo-Fr 17:30-22:30",
turnos == 'TARDE' ~ "Mo-Fr 13:00-17:00",
turnos == 'TARDE,VESPERTINO' ~ "Mo-Fr 13:00-21:30",
turnos == 'VESPERTINO' ~ "Mo-Fr 17:30-21:30",
TRUE ~ NA_character_
))
dosm$`opening_hours` <- df$`opening_hours`
#Escuelas cercanas
dosm$cercania <- df$cercania
dosm$cercanos <- df$cercanos
#Numero de telefono
df <- df %>%
mutate(
telefono = if_else(
!is.na(caracteristica_telefonica) & !is.na(nro_telefono) & nro_telefono != "" & caracteristica_telefonica != "",
paste0("+54 ", caracteristica_telefonica," ", nro_telefono),
paste0("No hay telefono registrado")
)
)
dosm$`phone` <- df$telefono
#Calles, calle derecha y izquierda
df <- df %>%
mutate(
calle_almacenada = if_else(
!is.na(calle_lateral_derecha) & !is.na(calle_lateral_izquierda) & calle_lateral_derecha != "" & calle_lateral_izquierda != "",
paste0(calle, " entre ", calle_lateral_derecha, " y ", calle_lateral_izquierda),
paste0(calle)
)
)
dosm$calle <- df$calle_almacenada
#problemas:
# Localidad
to_custom_title <- function(texto) {
# Palabras que deben quedar en minúscula (excepto si están al principio)
excepciones <- c("de", "la", "las", "los", "del", "y", "en", "el", "a", "al")
# Pasar todo a título
palabras <- strsplit(texto, " ")[[1]]
palabras <- str_to_title(palabras)
# Corregir excepciones (menos la primera palabra)
palabras[-1] <- ifelse(tolower(palabras[-1]) %in% excepciones,
tolower(palabras[-1]),
palabras[-1])
# Unir de nuevo
paste(palabras, collapse = " ")
}
#Primera mayuscula y el resto minuscula
dosm$name <- paste0(
toupper(substr(df$nombre, 1, 1)),
tolower(substr(df$nombre, 2, nchar(df$nombre)))
)
#Elimina el "N°"
dosm$name <- str_replace(dosm$name, "n°\\s*", "")
dosm$name <- str_replace(dosm$name, regex("n°\\s*", ignore_case = TRUE), "")
dosm$name <- str_replace(dosm$name, regex("n[º°]\\s*", ignore_case = TRUE), "")
#Eliminar las comillas
dosm$name <- str_replace_all(dosm$name, '"', "")
dosm$name <- str_to_title(dosm$name)
dosm$name <- sapply(dosm$name, to_custom_title)
dosm$`official_name` <- sapply(df$nombre, to_custom_title)
st_write(dosm, "MR_UEL_salida_final.geojson", driver = "GeoJSON")
## Writing layer `MR_UEL_salida_final' to data source
## `MR_UEL_salida_final.geojson' using driver `GeoJSON'
## Writing 19562 features with 19 fields and geometry type Point.
df <- df %>%
mutate(`addr:housenumber` = case_when(
nro == 'S/N' ~ "",
nro != 'S/N' ~ nro
))
dosm$`addr:housenumber` <- df$`addr:housenumber`
df <- df %>%
mutate(es_especial = ifelse(
str_detect(nombre, regex("ESPECIAL", ignore_case = TRUE)),
"yes",
""))
dosm$`education_for:disabled` <- df$es_especial
columnas_josm <- c("name","official_name","amenity","operator:type",
"addr:housenumber","ref:cue","ref:cui","isced:level","email","phone",
"addr:postcode","opening_hours","education_for:disabled","geometry")
djosm <- dosm[, c(columnas_josm)]
djosm$`addr:full` <- dosm$calle
st_write(djosm, "josm_UEL_salida_final.geojson", driver = "GeoJSON")
## Writing layer `josm_UEL_salida_final' to data source
## `josm_UEL_salida_final.geojson' using driver `GeoJSON'
## Writing 19562 features with 14 fields and geometry type Point.
mapview(djosm)